iT邦幫忙

2024 iThome 鐵人賽

DAY 2
0
JavaScript

Vue.js學習中的細節陷阱:30天自我學習指南系列 第 2

Day 2: Vue SFC樣板(Template)和渲染函式(Render Function)

  • 分享至 

  • xImage
  •  

上一篇為了深入Vue開啟了初章,也提到createAppApp mount掛載的宏觀概念,今天來研究官網有蜻蜓點水著墨,但並沒有刻意放在主章節上-Vue template樣板的本質和之前提及的渲染函數(Render Function)

因為章節篇幅關係,不能提及太多虛擬DOM介紹,有興趣的話看這篇文章回顧一下,是當初我還在上轉職課程的好文章。


今日學習重點:

  1. Vue的單一元件檔(SFC)如何工作,template 不是直接撰寫 HTML 語法
  2. Vue 的渲染函式是什麼,和虛擬DOM之間的關係?
  3. Vue 最小渲染單位是什麼,元件嗎?

開始今日的探索旅程~Go


在 Vue 3 開發中,我們經常使用官方建議的單文件元件(Single File Component, SFC)作為開發單位。

SFC 是一種將 <template<script><style> 用標籤形式組織在一起的文件結構,這樣開發者就可以用類似 HTML、CSS 和 JavaScript 的方式來撰寫元件。透過這種結構化的方式,開發者可以更直觀地編寫和維護元件,同時也能充分利用 Vue 的特性和工具鏈來進行開發。

// Vue SFC 基本程式碼
<script setup>
import { ref } from 'vue'
const greeting = ref('Hello World!')
</script>

<template>
  <p class="greeting">{{ greeting }}</p>
</template>

<style>
.greeting {
  color: red;
  font-weight: bold;
}
</style>

https://ithelp.ithome.com.tw/upload/images/20240915/20145251gS5yL3DdY8.png

這次我們將重點放在上次用作結尾的 Vue 渲染機制圖,並將其作為開頭的主題。

從中可以看到,SFC(單文件元件)的樣板會被轉換成渲染函式(render function),而這其中包含了一個編譯過程(compiler)。
如果你有興趣深入了解,可以直接進入 Vue SFC playground,你會發現雖然 SFC 的編寫看起來像是 HTML 代碼,但最終的轉換產物卻並不是 HTML 😯。

SFC 樣板的轉換過程可以概述為以下等式:

template 的最終編譯產物 = createElement (生成虛擬 DOM 節點 VNode) + 渲染函式 (render function)


樣板(Template)編譯的過程

官方文件中提到,SFC(單文件元件)文件實際上是一種特殊的格式,需要通過 @vue/compiler-sfc 編譯器來轉換成瀏覽器可以理解的 JavaScript 和 CSS。
這個編譯過程會將 .vue 文件中的 <template>轉換為渲染函式,並將 轉換為相應的 CSS 樣式。

編譯器裡主要有一段代碼是所謂的抽象語法樹(Abstract Syntax Tree,AST)

https://ithelp.ithome.com.tw/upload/images/20240915/201452517aa3lvlY0c.png
(圖片出處)

主要功能是將將模板轉換為一個能夠被程式語言處理的結構化數據,像是標籤種類、內容,我記得chrome瀏覽器V8引擎其中有一部分也是類似原理,就是將JS代碼轉成AST,之後後續才能進行JS運行和記憶體管理優化等。

樣板被解析轉成AST後,因為變成像JSON結構之後可以產生成為真正的Javascript Render function-渲染函式,至此這段過程是上次有提及的Vue 編譯器核心代碼(compiler core)幫我們完成的。


渲染函式(Render Function)是什麼?

理解到樣板<template>最終會被編譯成 JavaScript 程式碼後,就能打破在 Vue 裡面直接撰寫 HTML 的迷思。接下來,我們可以稍微探討一下渲染函式的內容:

除了樣板部分,<script> 標籤內的程式碼會被收集到 __sfc__ 物件中。

setup 是 Vue 3 Composition API 的語法糖,我們會在深入響應式系統時再詳細介紹。不過,當我們在 SFC 中定義事件處理函式(例如點擊事件的 function)refreactive 等內容時,這些都會被包含在 setup() 函式中,而這個函式的回傳值將允許你使用 __sfc__ 物件來調用內部的資料和方法,這樣的設計提供了更直觀的方式來組織和使用元件內的邏輯和狀態。

// SFC 文件檔編譯後所形成的JavaScript物件
const __sfc__ = {
  __name: 'App',
  setup(__props, { expose: __expose }) {
  __expose();

  const msg = ref('Hello World!')

  const __returned__ = { msg, ref }
  Object.defineProperty(__returned__, '__isScriptSetup', { enumerable: false, value: true })
  return __returned__
}

渲染函式(Render Function)的主體:

在 Vue 中,渲染函式的主體與生成虛擬節點(VNode)的函式密切相關,例如 createElementVNodecreateElementBlock 等,這些函式負責創建虛擬節點。
虛擬節點(VNode)是虛擬 DOM 的最小單位,這些節點最終會組合成一棵完整的虛擬 DOM 樹。

嚴格來說,在 Vue 中最小的渲染單位是虛擬節點(VNode),而不是元件

這是因為元件在編譯後可能會由多個虛擬節點組成,形成一個更高級別的抽象物件。

  • 虛擬節點是 Vue 進行 DOM 更新的基本單位,它們反映了真實 DOM 的結構。
  • 元件則是透過虛擬節點來管理,並且透過一些商業邏輯包裝來呈現其內部的視圖和邏輯。

因此,即使元件是 Vue 中的基本開發單位,真正參與渲染和更新的最小顆粒度應該是這些虛擬節點。

// Render Funciton 內呼叫其它創建虛擬DOM的方法
import { toDisplayString as _toDisplayString, createElementVNode as _createElementVNode, vModelText as _vModelText, withDirectives as _withDirectives, Fragment as _Fragment, openBlock as _openBlock, createElementBlock as _createElementBlock } from "vue"
function render(_ctx, _cache, $props, $setup, $data, $options) {
  return (_openBlock(), _createElementBlock(_Fragment, null, [
    _createElementVNode("h1", null, _toDisplayString($setup.msg), 1 /* TEXT */),
    _withDirectives(_createElementVNode("input", {
      "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (($setup.msg) = $event))
    }, null, 512 /* NEED_PATCH */), [
      [_vModelText, $setup.msg]
    ])
  ], 64 /* STABLE_FRAGMENT */))
}
__sfc__.render = render
__sfc__.__file = "src/App.vue"
export default __sfc__

實際上,Vue 也為開發者提供了一個直接操作生成渲染函式的 API——h() 函式

h() 函式類似於 JavaScript 版的超文本標記語言(Hypertext Markup Language),是一個封裝好的函式,用於生成虛擬 DOM 節點 (createVNode())。透過 h() 函式,開發者可以直接創建虛擬節點,而不需要操作複雜的底層邏輯,也不用透過撰寫SFC文件檔來做開發。

<script setup>
// 引入 Vue 的函式
import { ref, h } from 'vue';

// 定義計數器的狀態
const count = ref(0);

// 按鈕點擊事件處理函式
const increment = () => {
  count.value++;
};

// 渲染函式
const render = () =>
  h('div', { class: 'counter' }, [
    h('h1', '這是一個計數器範例'), // 使用 h() 生成標題節點
    h('button', { onClick: increment }, `點擊我:${count.value}`), // 使用 h() 生成按鈕節點
  ]);
</script>

<template>
  <!-- 使用 Vue 內建的渲染函式 render -->
  <component :is="render" />
</template>

雖然 h() 函式提供了高靈活性,但一般開發還是推薦使用 SFC,因為它更加直觀且易於維護。


帶有編譯訊息的Virtual DOM(Compiler-Informed Virtual DOM)

恩,好奇心,因為上面編譯完的渲染函式怎麼除了createVnode創建節點功能外,還有其它標記符號hoistingpatch flag?

一般傳統性Virtual DOM在需要更新比較新舊差異時,並不知道新的虛擬DOM和舊的虛擬DOM之間關係,會採用全部遍歷方式比對找出差異的部分,也因為如此即使改動一小段資料,其他保持不動的靜態樣板或資料也會將虛擬DOM一律重繪。

註解: react的一律重繪通常指的是這個部分,有點暴力性的作法但也帶來內存記憶體的消耗。

為了提升虛擬 DOM 的比較性能,Vue 3 在編譯器(compiler)中加入了幾項優化措施,使虛擬 DOM 的比對更加高效,這也是 Vue 官方推薦使用 SFC 文件模式開發的原因之一。

因為SFC 的編譯過程可以分析模板中的靜態和動態內容,在之後虛擬 DOM 比較時,能夠更精確地避免不必要的重繪,從而優化渲染性能。

簡單挑選幾項作介紹:

  • 靜態提升(Static hoisting)

同樣在模板編譯過程中,靜態節點會在編譯階段被標記為 /*HOISTED*/,這表示該虛擬節點(VNode)是靜態且不會改變的。

這些靜態節點在呼叫 createVNode時,會被提升(hoisted)並與其他需要更新的動態節點分開處理。在之後虛擬 DOM 進行前後差異比對時,這些靜態節點會被直接略過,不參與比對,從而大幅提升渲染性能。

const _hoisted_1 = /*#__PURE__*/_createElementVNode("p", null, /*#__PURE__*/_toDisplayString('123'), -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createElementVNode("p", null, /*#__PURE__*/_toDisplayString('456'), -1 /* HOISTED */)
  • 動態型資料綁定透過更新標記(patch-flag)

在樣板中帶有雙括號({{ }})綁定的資料如果是響應性的,Vue 在編譯時除了呼叫 createVNode() 系列函數外,還會在函數後的參數中加入更新標籤(patch flag)

_withDirectives(_createElementVNode("input", {
      "onUpdate:modelValue": _cache[0] || (_cache[0] = $event => (($setup.msg) = $event))
    }, null, 512 /* NEED_PATCH */), [
      [_vModelText, $setup.msg]
    ])
  ], 64 /* STABLE_FRAGMENT */))

createElementVNode("div", {
  class: _normalizeClass({ active: _ctx.active })
}, null, 2 /* CLASS */)

這些標籤使用 64 位元的二進位數來表示不同的分類,具體描述哪些部分是需要更新的。例如,2 的 6 次方(即 64)表示該節點不需要更新。 (patch flag 對照表)

之後Vue的執行核心(runtime core),根據渲覽函式產生出來的虛擬DOM節點(v node)所帶標記(flag),後續進行虛擬DOM差異比對(differ and reconcilliation),最終調用執行核心和瀏覽器相關DOM操作(runtime dom)的程式碼進行更新(patch)


總結:

  1. 本篇通過大量的 JavaScript 程式碼解析,打破了 Vue template 是撰寫 HTML 的迷思。

  2. Vue 的模板在經過 @vue/compiler-sfc編譯後,最終的產物是渲染函式(render function),而不是直接的 HTML。從中我們看到了 createVNode() 系列 API 如何生成虛擬節點,以及 Vue 中元件渲染的最小單位是這些虛擬DOM節點。

  3. Vue 3 的編譯核心(compiler-core)除了負責編譯外,還會在編譯過程中對 Vue template 中的靜態和動態節點進行分類,這樣可以在進入運行核心(runtime-core)時,提升虛擬 DOM 資料更新的計算效率。這種優化讓 Vue 能夠更精確地處理動態變化,提高整體渲染性能。

理解了開頭渲染機制圖中的渲染函式(render function)後,我們就可以開始深入探索 Vue 的響應式系統(reactive system)章節,進一步了解 Vue 如何處理數據變更與自動更新~繼續加油!


學習資源:

  1. https://medium.com/glovo-engineering/dissecting-vue-3-template-compilation-e01e2b98dafd (介紹Vue渲染函式觀念好文章,必讀)
  2. https://www.cythilya.tw/2017/03/31/virtual-dom/ (虛擬DOM觀念加強,超棒)
  3. https://feday.fequan.com/vueconf24/jinjiang_Lightning%20Show_VueConf%20CN%202024.pdf (Vue conf 2024- vue compiler貢獻者 趙錦江的ptt)
  4. https://www.youtube.com/watch?v=cpGZgKz-SnM (尤雨溪親自講解 Vue Template,超愛~講解超親民~~但為什麼觀看人數超少XD)

上一篇
Day 1: Vue Create App and Mount
下一篇
Day 3: <script setup> 語法糖的本質
系列文
Vue.js學習中的細節陷阱:30天自我學習指南30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言